﻿/*
    Copyright (C) 2011 Andreas Töpfer

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    For details see <http://www.gnu.org/licenses/>.
 */

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.IO;
using System.Collections;
using System.Threading;
using System.Security.Cryptography;
using System.Diagnostics;
using System.Web;
using System.Web.UI;

namespace fileVerifier
{
	public delegate void updateFileProgress(int progress);
	public delegate void updateTotalProgress(int progress);
	public delegate void resetQuitBtn();

	public partial class Form1 : Form
	{
		private Thread workerThread;
		private string[] fileTypes, targetDirectories;
		private bool cancelFlag = false, withCompare, withCalculate, includeSubfolders, openReport;
		string reportPath;
			

		public Form1()
		{
			InitializeComponent();
		}

		private void startBtn_Click(object sender, EventArgs e)
		{
			if (workerThread == null || (workerThread != null && !workerThread.IsAlive))
			{
				cancelFlag = false;
				workerThread = new Thread(this.DoWork);
				targetDirectories = new string[dirListBox.Items.Count];
				withCompare = compareFilesChk.Checked;
				withCalculate = createFilesChk.Checked;
				includeSubfolders = includeSubfoldersChk.Checked;
				openReport = openReportChk.Checked;

				// clean up and split filetype list
				if (fileTypesBox.TextLength == 0) fileTypes = new string[1] {"allFiles"};
				else fileTypes = fileTypesBox.Text.Replace(".", "").Replace(" ", "").Split(',');

				for (int i = 0; i < dirListBox.Items.Count; i++)
				{
					targetDirectories[i] = dirListBox.Items[i].ToString();
				}

				workerThread.Start();
				quitBtn.Text = "Cancel";
				totalProgressBar.Value = 1;
			}
		}

		private void addDirBtn_Click(object sender, EventArgs e)
		{
			if (folderBrowserDialog1.ShowDialog() == DialogResult.OK)
			{
				dirListBox.Items.Add(folderBrowserDialog1.SelectedPath);
			}
		}

		private void removeDirBtn_Click(object sender, EventArgs e)
		{
			if (dirListBox.SelectedIndex >= 0)
			{
				dirListBox.Items.RemoveAt(dirListBox.SelectedIndex);
			}
		}

		private void clearDirListBtn_Click(object sender, EventArgs e)
		{
			dirListBox.Items.Clear();
		}

		private void browseReportPathBtn_Click(object sender, EventArgs e)
		{
			if (folderBrowserDialog1.ShowDialog() == DialogResult.OK)
			{
				reportPathLbl.Text = folderBrowserDialog1.SelectedPath;
				toolTip1.SetToolTip(reportPathLbl, folderBrowserDialog1.SelectedPath);
			}
		}

		private void quitBtn_Click(object sender, EventArgs e)
		{
			if (workerThread != null && workerThread.IsAlive)
			{
				if (MessageBox.Show("Are you sure you want to cancel the scanning process?", "Cancel", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
				{
					cancelFlag = true;
				}
				workerThread.Join();
			}
			else
			{
				this.Close();
			}
		}

		private void resetQuitBtn()
		{
			quitBtn.Text = "Quit";
		}

		private void Form1_FormClosing(object sender, FormClosingEventArgs e)
		{
			if (workerThread != null && workerThread.IsAlive)
			{
				if (MessageBox.Show("Are you sure you want to cancel the scanning process?", "Cancel", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
				{
					cancelFlag = true;

				}
				else
				{
					e.Cancel = true;
				}

			}

			if (dirListBox.Items.Count > 0)
			{
				StringBuilder targetDirectories = new StringBuilder();
				// get target directories from listbox
				targetDirectories.Append(dirListBox.Items[0].ToString());
				for (int i = 1; i < dirListBox.Items.Count; i++)
				{
					targetDirectories.Append("," + dirListBox.Items[i].ToString());
				}
				Properties.Settings.Default.directoryList = targetDirectories.ToString();
			}
			else
			{
				Properties.Settings.Default.directoryList = "";
			}
			Properties.Settings.Default.fileTypes = fileTypesBox.Text;
			Properties.Settings.Default.compareFiles = compareFilesChk.Checked;
			Properties.Settings.Default.createFiles = compareFilesChk.Checked;
			Properties.Settings.Default.compareFiles = createFilesChk.Checked;
			Properties.Settings.Default.includeSubfolders = includeSubfoldersChk.Checked;
			Properties.Settings.Default.openReport = openReportChk.Checked;
			Properties.Settings.Default.reportPath = reportPathLbl.Text;
			Properties.Settings.Default.Save();

			// wait for thread if it's still running
			if (cancelFlag) workerThread.Join();
		}

		private void updateFileProgress(int progress)
		{
			if (progress >= 0 && progress <= 100)
			{
				// go backwards to update progressbar instantly
				fileProgressBar.Value = progress+1;
				fileProgressBar.Value = progress;
			}
		}

		private void updateTotalProgress(int progress)
		{
			if (progress >= 0 && progress <= 100)
			{
				// go backwards to update progressbar instantly
				totalProgressBar.Value = progress + 1;
				totalProgressBar.Value = progress;
			}

		}

		private void Form1_Load(object sender, EventArgs e)
		{
			if (!Properties.Settings.Default.directoryList.Length.Equals(0))
			{
				string[] targetdirectories = Properties.Settings.Default.directoryList.Split(',');
				foreach (string targetDirectory in targetdirectories)
				{
					dirListBox.Items.Add(targetDirectory);
				}
			}
			fileTypesBox.Text = Properties.Settings.Default.fileTypes;
			compareFilesChk.Checked = Properties.Settings.Default.compareFiles;
			compareFilesChk.Checked = Properties.Settings.Default.createFiles;
			createFilesChk.Checked = Properties.Settings.Default.compareFiles;
			includeSubfoldersChk.Checked = Properties.Settings.Default.includeSubfolders;
			openReportChk.Checked = Properties.Settings.Default.openReport;

			reportPath = Application.StartupPath + "\\reports";
			try
			{
				if (!Directory.Exists(reportPath))
				{
					Directory.CreateDirectory(reportPath);
				}
			}
			catch (Exception ex)
			{
				MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
			}
			reportPathLbl.Text = reportPath;
		}

		private string[] directoryRecursion(String currentDirectory, bool includeSubfolders)
		{
			ArrayList filePaths = new ArrayList();
			try
			{
				foreach (String currentFile in Directory.GetFiles(currentDirectory))
				{
					// only get selected file types or all files, ignore md5 files
					if ((fileTypes[0] == "allFiles" || fileTypes.Contains(Path.GetExtension(currentFile).Replace(".", ""))) && Path.GetExtension(currentFile) != ".md5")
					{
						filePaths.Add(Path.GetFullPath(currentFile));
					}
				}
				filePaths.Sort();

				if (includeSubfolders)
				{
					ArrayList subDirectories = new ArrayList();

					foreach (String subDirectory in Directory.GetDirectories(currentDirectory))
					{
						if (!Path.GetFullPath(subDirectory).Contains("$RECYCLE.BIN") && !Path.GetFullPath(subDirectory).Contains("System Volume Information"))
						{
							subDirectories.Add(subDirectory);
						}
					}
					subDirectories.Sort();
					foreach(String subDirectory in subDirectories)
					{
						// recursion
						filePaths.AddRange(directoryRecursion(subDirectory, true));
					}
				}
			}
			catch (Exception ex)
			{
				MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
			}
			// pfade aus aktuellem verzeichnis und unterverzeichnissen zurückgeben
			return (string[])filePaths.ToArray(typeof(string));
		}

		public void DoWork()
		{
			try
			{
				string calculatedHash, savedHash = null, hashFilePath, currentDir = "", rowColor = "dark";
				ArrayList filePaths = new ArrayList(), filePathsDistinct = new ArrayList();
				bool hashFileExists;
				int fileCount = 0, filesProcessed = 0, passedCount = 0, failedCount = 0, totalFailedCount = 0, createdCount = 0, dirNumber = 0;

				// create list of files to process
				foreach (string targetDirectory in targetDirectories)
				{
					filePaths.AddRange(directoryRecursion(targetDirectory, includeSubfolders));
				}

				if (filePaths.Count > 0)
				{
					// eliminate duplicates
					foreach (string filePath in filePaths)
					{
						if (!filePathsDistinct.Contains(filePath))
						{
							filePathsDistinct.Add(filePath);
							fileCount++;
						}
					}
					filePaths = filePathsDistinct;

					// store starting time
					string started = DateTime.Now.ToShortTimeString();

					// create report file
					StringWriter writer = new StringWriter();
					writer.Write("<html>\r\n<head>\r\n<title>Dynamic File Verifier - Scan Results " + DateTime.Now.ToString() + "</title>\r\n<link href=\"..\\inc\\style.css\" type=\"text/css\" rel=\"stylesheet\" />\r\n<script language=\"JavaScript\" src=\"..\\inc\\script.js\"></script>\r\n</head>\r\n<body>\r\n");

					// open first html table
					currentDir = Path.GetDirectoryName(filePaths[0].ToString());
					writer.Write("<div class=\"directory\"><a href=\"#\" onClick=\"toggledisplay(" + dirNumber + "); return none;\">" + currentDir + "</a></div>\r\n");
					writer.Write("<table id=\"" + dirNumber + "\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\r\n");
					writer.Write("<tr class=\"light\">\r\n<td>Result</td>\r\n<td>Filename</td>\r\n<td>Calculated hash</td>\r\n<td>Stored hash</td>\r\n</tr>\r\n");

					foreach (string filePath in filePaths)
					{

						if (currentDir != Path.GetDirectoryName(filePath))
						{
							writer.Write("</table>\r\nDirectory summary: <span class=\"passed\">" + passedCount + "</span>passed <span class=\"created\">" + createdCount + "</span>created <span class=\"failed\">" + failedCount + "</span>failed\r\n");

							currentDir = Path.GetDirectoryName(filePath);
							rowColor = "dark";
							dirNumber++;
							passedCount = 0;
							failedCount = 0;
							createdCount = 0;

							writer.Write("<div class=\"directory\"><a href=\"#\" onClick=\"toggledisplay(" + dirNumber + "); return none;\">" + currentDir + "</a></div>\r\n");
							writer.Write("<table id=\"" + dirNumber + "\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\r\n");
							writer.Write("<tr class=\"light\">\r\n<td>Result</td>\r\n<td>Filename</td>\r\n<td>Calculated hash</td>\r\n<td>Stored hash</td>\r\n</tr>\r\n");
						}

						hashFilePath = Path.GetDirectoryName(filePath) + "\\" + Path.GetFileNameWithoutExtension(filePath) + ".md5";
						hashFileExists = File.Exists(hashFilePath);

						if (hashFileExists && withCompare)
						{
							calculatedHash = file2MD5(filePath);

							if (cancelFlag)
							{
								break;
							}
							try
							{
								// get previously calculated hash
								StreamReader sr = new StreamReader(hashFilePath, System.Text.Encoding.ASCII);
								savedHash = sr.ReadLine().Substring(0, 32);
								sr.Close();
							}
							catch (Exception ex)
							{
								MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
							}
							finally
							{
								if (savedHash == calculatedHash)
								{
									writer.Write("<tr class=\"" + rowColor + "\">\r\n<td class=\"passed\">passed</td>\r\n<td>" + Path.GetFileName(filePath) + "</td>\r\n<td>" + calculatedHash + "</td>\r\n<td>" + savedHash + "</td>\r\n</tr>\r\n");
									passedCount++;
								}
								else
								{
									writer.Write("<tr class=\"" + rowColor + "\">\r\n<td class=\"failed\">failed</td>\r\n<td>" + Path.GetFileName(filePath) + "</td>\r\n<td>" + calculatedHash + "</td>\r\n<td>" + savedHash + "</td>\r\n</tr>\r\n");
									failedCount++;
									totalFailedCount++;
								}

								if (rowColor == "dark") rowColor = "light";
								else rowColor = "dark";
							}

						}
						else if (!hashFileExists && withCalculate)
						{
							calculatedHash = file2MD5(filePath);

							if (cancelFlag)
							{
								break;
							}
							if (calculatedHash == "null")
							{
								writer.Write("<tr class=\"" + rowColor + "\">\r\n<td>empty file</td>\r\n<td>" + Path.GetFileName(filePath) + "</td>\r\n<td>none</td>\r\n<td>none</td>\r\n</tr>\r\n");
							}
							else
							{
								try
								{
									// save hash
									StreamWriter sw = new StreamWriter(hashFilePath, false, System.Text.Encoding.ASCII);
									sw.WriteLine(calculatedHash);
									sw.Close();
								}
								catch (Exception ex)
								{
									MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
								}
								finally
								{
									writer.Write("<tr class=\"" + rowColor + "\">\r\n<td class=\"created\">analyzed</td>\r\n<td>" + Path.GetFileName(filePath) + "</td>\r\n<td>" + calculatedHash + "</td>\r\n<td>none</td>\r\n</tr>\r\n");
									createdCount++;
								}
							}
							if (rowColor == "dark") rowColor = "light";
							else rowColor = "dark";

						}
						filesProcessed++;
						totalProgressBar.BeginInvoke(new updateTotalProgress(updateTotalProgress), (int)((double)100 / fileCount * filesProcessed));
					}

					writer.Write("</table>\r\nDirectory summary: <span class=\"passed\">" + passedCount + "</span>passed <span class=\"created\">" + createdCount + "</span>created <span class=\"failed\">" + failedCount + "</span>failed\r\n");
					if (totalFailedCount == 0) writer.Write("<div class=\"passed\"><div class=\"stats\">File Status: all passed<br>");
					else writer.Write("<div class=\"failed\"><div class=\"stats\">File Status: " + totalFailedCount + " failed<br>");

					writer.Write("File verification was started at " + started + " and finished at " + DateTime.Now.ToShortTimeString() + " on " + DateTime.Now.ToShortDateString() + "<br>The following root directories have been analyzed: ");

					if (targetDirectories.Count() > 1)
					{
						for (int i = 0; i < (targetDirectories.Count() - 1); i++)
						{
							writer.Write(targetDirectories[i] + ", ");
						}
					}
					writer.Write(targetDirectories[targetDirectories.Count() - 1]);

					writer.Write("</div></div>\r\n</body>\r\n</html>");
					string reportFile = reportPath + "\\Dynamic File Verifier - Scan Results " + DateTime.Now.ToString("yyyyMMdd-HHmm") + ".html";
					StreamWriter logWriter = new StreamWriter(reportFile, false, System.Text.Encoding.ASCII);
					logWriter.Write(writer.ToString());
					logWriter.Close();
					if (openReport) Process.Start(reportFile);
				}
				else 
				{
					MessageBox.Show("No files that match the current filters have been found.", "No files to process", MessageBoxButtons.OK, MessageBoxIcon.Warning);
				}
			}
			catch (Exception ex)
			{
				MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
			}
			fileProgressBar.BeginInvoke(new updateFileProgress(updateFileProgress), 0);
			totalProgressBar.BeginInvoke(new updateTotalProgress(updateTotalProgress), 0);
			quitBtn.BeginInvoke(new resetQuitBtn(resetQuitBtn));
		}

		public string file2MD5(string filePath)
		{
			byte[] md5Hash = null, buffer, oldBuffer;
			int bytesRead, oldBytesRead;
			long size, totalBytesRead = 0;

			//DateTime begin = DateTime.UtcNow;

			try
			{
				//datei einlesen
				System.IO.FileStream fs = System.IO.File.OpenRead(filePath);
				System.Security.Cryptography.MD5 md5 = new System.Security.Cryptography.MD5CryptoServiceProvider();

				size = fs.Length;
				buffer = new byte[1048576]; // 1 MB
				bytesRead = fs.Read(buffer, 0, buffer.Length);
				totalBytesRead += bytesRead;

				if (bytesRead == 0) return "null";
				do
				{
					oldBytesRead = bytesRead;
					oldBuffer = buffer;

					buffer = new byte[1048576];
					bytesRead = fs.Read(buffer, 0, buffer.Length);
					totalBytesRead += bytesRead;

					if (bytesRead == 0)
					{
						md5.TransformFinalBlock(oldBuffer, 0, oldBytesRead);
					}
					else
					{
						md5.TransformBlock(oldBuffer, 0, oldBytesRead, oldBuffer, 0);
					}
					if (cancelFlag)
					{
						return "";
					}
					fileProgressBar.BeginInvoke(new updateFileProgress(updateFileProgress), (int)((double)100 / size * totalBytesRead));
				} while (bytesRead != 0);

				fs.Close();

				md5Hash = md5.Hash;
				fileProgressBar.BeginInvoke(new updateFileProgress(updateFileProgress), 0);
			}
			catch (Exception ex)
			{
				MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
			}

			//DateTime end = DateTime.UtcNow;
			//MessageBox.Show("Measured time: " + (end-begin).TotalSeconds + " sec.");

			return BitConverter.ToString(md5Hash).Replace("-", "").ToLower();
		}

		private void reportPathLbl_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
		{
			Process.Start(reportPathLbl.Text);
		}
	}
}